package server

import (
	"time"
	"github.com/sirupsen/logrus"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/ingopansa/userinfoservice/storage"
	"context"
	"net/url"
	"fmt"
	"errors"
	"net/http"
	"strconv"
	"github.com/gorilla/mux"
	"sync/atomic"
	"github.com/felixge/httpsnoop"
	"github.com/ingopansa/userinfoservice/tokenprovider"
)

type Route struct {
	Name        string
	Method      string
	Pattern     string
	HandlerFunc http.HandlerFunc
}

type Routes 	[]Route

type Server struct {
	issuerURL 		url.URL
	storage 		storage.Storage

	endpoints		[]Endpoint

	now func() 		time.Time
	mux 			http.Handler
	logger 			logrus.FieldLogger

	routes 			Routes
}

type Endpoint struct {
	Server			*Server			// back reference to teh server config
	BasePath		string
	TokenProvider 	tokenprovider.TokenProvider
}

// Config holds the server's configuration options.
//
// Multiple servers using the same storage are expected to be configured identically.
type Config struct {
	Issuer 				string

	// The backing persistence layer.
	Storage 			storage.Storage

	// The backing OAuth TokenProvider
	//TokenProviders 		[]tokenprovider.TokenProvider

	Endpoints			[]Endpoint

	RotateKeysAfter  	time.Duration // Defaults to 6 hours.
	IDTokensValidFor	time.Duration // Defaults to 24 hours

	// If specified, the server will use this function for determining time.
	Now func() 			time.Time

	Logger 				logrus.FieldLogger

	PrometheusRegistry 	*prometheus.Registry
}

func value(val, defaultValue time.Duration) time.Duration {
	if val == 0 {
		return defaultValue
	}
	return val
}

func ( s *Server )newRouter(config Config) (*mux.Router, error) {
	router := mux.NewRouter().StrictSlash(true)

	requestCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "http_requests_total",
		Help: "Count of all HTTP requests.",
	}, []string{"handler", "code", "method"})

	err := config.PrometheusRegistry.Register(requestCounter)
	if err != nil {
		return nil, fmt.Errorf("server: Failed to register Prometheus HTTP metrics: %v", err)
	}

	instrumentHandlerCounter := func(handlerName string, handler http.Handler) http.HandlerFunc {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			m := httpsnoop.CaptureMetrics(handler, w, r)
			requestCounter.With(prometheus.Labels{"handler": handlerName, "code": strconv.Itoa(m.Code), "method": r.Method}).Inc()
		})
	}

	for _, route := range s.routes {
		var handler http.Handler
		handler = route.HandlerFunc
		handler = instrumentHandlerCounter(route.Name, handler)
		router.
			Methods(route.Method).
			Path(route.Pattern).
			Name(route.Name).
			Handler(handler)

	}
	return router, nil
}

// NewServer constructs a server from the provided config.
func NewServer(ctx context.Context, c Config) (*Server, error) {
	return newServer(ctx, c, defaultRotationStrategy(
		value(c.RotateKeysAfter, 6*time.Hour),
		value(c.IDTokensValidFor, 24*time.Hour)))
}

func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) (*Server, error) {
	issuerURL, err := url.Parse(c.Issuer)
	if err != nil {
		return nil, fmt.Errorf("server: can't parse issuer URL")
	}

	if c.Storage == nil {
		return nil, errors.New("server: storage cannot be nil")
	}

	if c.Endpoints == nil {
		return nil, errors.New( "server: endpoints cannot be nil")
	}

	now := c.Now
	if now == nil {
		now = time.Now
	}

	// configure the server
	server := &Server{
		issuerURL: 				*issuerURL,
		storage:                newKeyCacher(c.Storage, now),
		endpoints:				c.Endpoints,
		now:                    now,
		logger:                 c.Logger,
		routes:					Routes{},
	}

	server.routes = append(server.routes, Route{
		Name: "Health check",
		Method: "GET",
		Pattern: "/health",
		HandlerFunc: server.handleHealth,
	})

	server.routes = append(server.routes, Route{
		Name: "List all users",
		Method: "GET",
		Pattern: "/users",
		HandlerFunc: server.handleListUsers,
	})

	server.routes = append(server.routes, Route{
		Name: "Add one user",
		Method: "POST",
		Pattern: "/users",
		HandlerFunc: server.handleAddUser,
	})

	for _, ep := range c.Endpoints {
		server.logger.Debugf( "adding route handler for: %s", ep.BasePath )
		//server.logger.Debugf( "%v", ep.TokenProvider )

		ep.Server = server

		server.routes = append(server.routes, Route{
			Name: "Userinfo endpoint",
			Method: "GET",
			Pattern: ep.BasePath,
			HandlerFunc: ep.handleUserInfoEndpoint,
		})
	}



	if router, err := server.newRouter(c); err != nil {
		server.logger.Errorf("Cannot start mux router")
		return nil, err
	} else {
		router.NotFoundHandler = http.HandlerFunc(http.NotFound)
		server.mux = router
	}

	//server.startKeyRotation(ctx, rotationStrategy, now)

	return server, nil
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.mux.ServeHTTP(w, r)
}

// newKeyCacher returns a storage which caches keys so long as the next
func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage {
	if now == nil {
		now = time.Now
	}
	return &keyCacher{Storage: s, now: now}
}

type keyCacher struct {
	storage.Storage

	now  func() time.Time
	keys atomic.Value // Always holds nil or type *storage.Keys.
}

func (k *keyCacher) GetKeys() (storage.Keys, error) {
	keys, ok := k.keys.Load().(*storage.Keys)
	if ok && keys != nil && k.now().Before(keys.NextRotation) {
		return *keys, nil
	}

	storageKeys, err := k.Storage.GetKeys()
	if err != nil {
		return storageKeys, err
	}

	if k.now().Before(storageKeys.NextRotation) {
		k.keys.Store(&storageKeys)
	}
	return storageKeys, nil
}